﻿using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;

using LuckyBingoRcon.Matches;

namespace LuckyBingoRcon.Commands
{
    public abstract class Command
    {
        protected string password = null;
        protected string commandText = null;
        protected string[] args = null;

        protected MapLookup mapLookup = null;
        protected GametypeLookup gametypeLookup = null;
        protected Encoding encoding = Encoding.ASCII;

        private UdpClient uc = new UdpClient();

        // explicit destructor to clean up network connections
        ~Command()
        {
            try
            {
                uc.Close();
            }
            catch
            {

            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="serverEp"></param>
        public void Submit(IPEndPoint serverEp)
        {
            if (serverEp == null)
                return;

            /*
             * Send to server
             */
            uc.Client.ReceiveTimeout = 10000;
            uc.Connect(serverEp);

            byte[] bytes = null;
            if (args == null)
            {
                bytes = this.Encode(password + " " + commandText);
            }
            else
            {
                bytes = this.Encode(password + " " + commandText, args);
            }

            uc.Send(bytes, bytes.Length);

            /*
             * Capture server response on this thread
             */
            MemoryStream buffer = new MemoryStream();
            BinaryWriter bufferWriter = new BinaryWriter(buffer);

            IPEndPoint respondingEp = null;
            byte[] respBytes = uc.Receive(ref respondingEp);
            if (!this.ValidateResponse(respBytes, serverEp, respondingEp))
            {
                bufferWriter.Close();
                return;
            }

            bufferWriter.Write(respBytes, 5, respBytes.Length - 5);

            bool keepListening = true;
            uc.Client.ReceiveTimeout = 1000;
            while (keepListening)
            {
                try
                {
                    byte[] nextReceive = uc.Receive(ref respondingEp);
                    if (!this.ValidateResponse(nextReceive, serverEp, respondingEp))
                    {
                        bufferWriter.Close();
                        return;
                    }

                    bufferWriter.Write(nextReceive, 5, nextReceive.Length - 5);
                }
                catch (SocketException)	// catch the timeout error
                {
                    keepListening = false;
                }
            }

            buffer.Position = 0;
            StreamReader bufferReader = new StreamReader(buffer, encoding);
            StringBuilder sb = new StringBuilder();

            while (bufferReader.Peek() > 0)	// can get away with this because no COD packets have nulls inside of lines
            {
                string line = bufferReader.ReadLine();
                if (!String.IsNullOrEmpty(line))
                {
                    sb.AppendLine(line);
                }
            }

            bufferWriter.Close();
            bufferReader.Close();
            buffer.Close();

            this.CommandOnResponse(sb.ToString());
        }

        public void Cancel()
        {
            try
            {
                uc.Close();
				uc = new UdpClient();
            }
            catch
            {
                // eat any errors
            }
        }

        protected virtual byte[] Encode(string command)
        {

            byte[] bytes = new byte[command.Length + 5];

            bytes[0] = 255;
            bytes[1] = 255;
            bytes[2] = 255;
            bytes[3] = 255;
            bytes[4] = 0;

            {
                byte[] cmdBytes = this.encoding.GetBytes(command);
                cmdBytes.CopyTo(bytes, 5);
            }
            return bytes;
        }

        protected virtual byte[] Encode(string command, string[] args)
        {
            // We need to put spaces in between arguments
            // To make sure we have enough bytes, add a space within the for loop, then remove the last one outside the loop
            int[] argsLength = new int[args.Length];
            int totalArgsLength = 0;
            int bytePointer = 0;
            for (int i = 0; i < args.Length; i++)
            {
                argsLength[i] = args[i].Length + 1;             // +1 for the space after the argument in case another follows
                totalArgsLength += args[i].Length + 1;          // +1 for the space after the argument in case another follows
            }
            argsLength[args.Length - 1]--;                      // Remove the space at the end of the last argument
            totalArgsLength--;                                  // Remove the space at the end of the last argument

            byte[] bytes = new byte[command.Length + totalArgsLength + 5];

            bytes[0] = 255;
            bytes[1] = 255;
            bytes[2] = 255;
            bytes[3] = 255;
            bytes[4] = 0;

            bytePointer += 5;

            {
                byte[] cmdBytes = this.encoding.GetBytes(command);
                cmdBytes.CopyTo(bytes, bytePointer);

                bytePointer += cmdBytes.Length;
                byte[] argBytes;

                string arg;
                for (int i = 0; i < args.Length; i++)
                {
                    if (i.Equals(args.Length - 1))
                        arg = args[i];
                    else
                        arg = args[i] + " ";
                    argBytes = this.encoding.GetBytes(arg);
                    argBytes.CopyTo(bytes, bytePointer);
                    bytePointer += argsLength[i];
                }
            }

            return bytes;
        }

        public Encoding Encoding
        {
            get
            {
                if (encoding == null)
                {
                    encoding = Encoding.ASCII;
                }

                return encoding;
            }

            set
            {
                if (value != null)
                {
                    encoding = value;
                }
            }
        }

        private bool ValidateResponse(byte[] response)
        {
            try
            {
                return
                    response[0] == 255 &&
                    response[1] == 255 &&
                    response[2] == 255 &&
                    response[3] == 255 &&
                    response[4] == 1;
            }
            catch
            {
                return false;
            }
        }

        private bool ValidateResponse(byte[] response, IPEndPoint remoteEp, IPEndPoint respondingEp)
        {
            try
            {
                return this.ValidateResponse(response) &&
                    respondingEp.Address.ToString().Equals(remoteEp.Address.ToString());
            }
            catch
            {
                return false;
            }
        }

        public string Password
        {
            get { return password; }
            set { password = value; }
        }

        public string[] Args
        {
            get { return args; }
            set { args = value; }
        }

        public MapLookup MapLookup
        {
            get { return mapLookup; }
            set { mapLookup = value; }
        }

        public GametypeLookup GametypeLookup
        {
            get { return gametypeLookup; }
            set { gametypeLookup = value; }
        }

        /// <summary>
        /// Implementation details left to extending classes. This model
        /// assumes that the implementation will be using an event driven
        /// model to pass results around.
        /// </summary>
        /// <param name="response">Gauranteed to have no empty lines</param>
        protected abstract void CommandOnResponse(string response);
    }
}

